//
//    rsqd - RsQueue daemon responsible for queue.
//    Copyright (C) 2022  Marcus Pedersén marcus@marcux.org
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
//


use actix_web::{
    error, get, post,
    http::header::{self, ContentType},
    middleware, web, App, HttpResponse, HttpRequest, HttpServer
};

use serde_json::json;
use serde::{Serialize, Deserialize};
use passwords::PasswordGenerator;
use log::{info, warn, error};
use dns_lookup::lookup_addr;
use std::io::Write;
use rsqlib::client::{
    RsqRequest,
    RsqCommand,
    RsqJob,
    RsqState,
    RsqdResponse
};
use rsqlib::all::*;
mod db;


/// Config and default values
const CONFIG_FILE: &str = "/etc/rsqueue/rsqd.conf";
const PORT: u16 = 9921;
const DB_FILE: &str = "/var/lib/rsqueue/rsqueue.db";
const RSQD_LOG: &str = "/var/log/rsqueue/rsqueue-daemon.log";
const DB_BACKUP_DIRECTORY: &str = "/var/lib/rsqueue/backup/";
const DB_BACKUP_HOUR: u8 = 2;
const DB_BACKUP_MINUTE: u8 = 0;
const DB_BACKUP_MAX_COUNT: u32 = 5;



/// Contains configuration
/// read from rsqd.conf file.
#[derive(Debug, Deserialize)]
struct Config {
    port: Option<u16>,
    db_file: Option<String>,
    rsqd_log: Option<String>,
    db_backup_directory: Option<String>,
    db_backup_hour: Option<u8>,
    db_backup_minute: Option<u8>,
    db_backup_max_count: Option<u32>,
}


#[derive(Serialize, Deserialize)]
struct JobStatus {
    key: String,
    jobid: u32,
}


/// Returns Config read from
/// rsqd.conf, with default
/// values populated if values
/// are missing in file.
fn get_config() -> Result<Config, (Config, String)> {
    let def_conf = Config {
	port: Some(PORT),
	db_file: Some(DB_FILE.to_string()),
	rsqd_log: Some(RSQD_LOG.to_string()),
	db_backup_directory: Some(DB_BACKUP_DIRECTORY.to_string()),
	db_backup_hour: Some(DB_BACKUP_HOUR),
	db_backup_minute: Some(DB_BACKUP_MINUTE),
	db_backup_max_count: Some(DB_BACKUP_MAX_COUNT) ,
    };


    match std::fs::read_to_string(CONFIG_FILE) {
	Ok(cont) => {
	    match toml::from_str::<Config>(&cont) {
		Ok(mut c) => {
		    if c.port == None {
			c.port = Some(PORT);
		    }
		    
		    if c.db_file == None {
			c.db_file = Some(DB_FILE.to_string());
		    }

		    if c.rsqd_log == None {
			c.rsqd_log = Some(RSQD_LOG.to_string());
		    }

		    if c.db_backup_directory == None {
			c.db_backup_directory = Some(DB_BACKUP_DIRECTORY.to_string());
		    }
		    
		    if c.db_backup_hour == None {
			c.db_backup_hour = Some(DB_BACKUP_HOUR);
		    }
		    
		    if c.db_backup_minute == None {
			c.db_backup_minute = Some(DB_BACKUP_MINUTE);
		    }
		    
		    if c.db_backup_max_count == None {
			c.db_backup_max_count = Some(DB_BACKUP_MAX_COUNT);
		    }

		    return Ok(c);
		}
		Err(e) => {
		    return Err((def_conf,
				format!("Failed to parse config file: {}\n{}\nCheck configuration",
					CONFIG_FILE, e)));
		}
	    }
	},
	Err(e) => {
	    return Err((def_conf,
			format!("Failed to read config file: {}\n{}\nCheck configuration",
				CONFIG_FILE, e)));
	}		
    }
}



/// If valid RsqRequest
/// is given as argument
/// then the rsq queue
/// is returned as a json
async fn rsq_queue(rsq: &RsqRequest) -> HttpResponse {
    match rsq.cmd {
	RsqCommand::ShowQueue {long} => {
	    let jobs: Vec<RsqJob> = vec!(
		RsqJob {
		    jobid: 1,
		    user: "Nisse".to_string(),
		    name: Some("The name".to_string()),
		    init: "2022-10-30 22:43".to_string(),
		    start: None,
		    end: None,
		    depend: None,
		    state: RsqState::Queued,
		    cores_alloc: None,
		    nodes_alloc: None,
		    nodes: None,
		    no_tasks: 8,
		    no_tasks_exec: 0,
		    no_tasks_term: 0,
		    no_tasks_err: 0,
		    tasks: Vec::new(),
		}
	    );

	    let response = RsqdResponse::ShowQueue { queue: jobs };



	    println!("returning queue long: {}", long);
	    HttpResponse::Ok().json(
                json!(&response))


	},
	_ => default_handler().await,
    }
}



/// If valid RsqRequest
/// is given as argument
/// then the rsq history
/// is returned as a json
async fn rsq_history(rsq: &RsqRequest) -> HttpResponse {
    match rsq.cmd {
	RsqCommand::ShowHistory {long, limit} => {
	    // limit numbers to return by limit
	    let jobs: Vec<RsqJob> = vec!(
		RsqJob {
		    jobid: 1,
		    user: "Nisse".to_string(),
		    name: Some("The history name".to_string()),
		    init: "2022-10-30 22:43".to_string(),
		    start: Some("2022-03-19 13:32:32".to_string()),
		    end: Some("2022-04-29 19:28:02".to_string()),
		    depend: None,
		    state: RsqState::Error,
		    cores_alloc: None,
		    nodes_alloc: None,
		    nodes: None,
		    no_tasks: 8,
		    no_tasks_exec: 0,
		    no_tasks_term: 7,
		    no_tasks_err: 1,
		    tasks: Vec::new(),
		}
	    );

	    let response = RsqdResponse::ShowHistory { history: jobs };



	    println!("returning history long: {}", long);
	    HttpResponse::Ok().json(
                json!(&response))


	},
	_ => default_handler().await,
    }
}



/// If valid RsqRequest
/// is given as argument
/// then the rsq job
/// is returned as a json
async fn rsq_show_job(rsq: &RsqRequest) -> HttpResponse {
    match rsq.cmd {
	RsqCommand::ShowJob {details, id} => {
	    let job =
		RsqJob {
		    jobid: 100,
		    user: "Nisse".to_string(),
		    name: Some("The name show job".to_string()),
		    init: "2022-10-30 22:43".to_string(),
		    start: None,
		    end: None,
		    depend: None,
		    state: RsqState::Queued,
		    cores_alloc: None,
		    nodes_alloc: None,
		    nodes: None,
		    no_tasks: 8,
		    no_tasks_exec: 0,
		    no_tasks_term: 0,
		    no_tasks_err: 0,
		    tasks: Vec::new(),
		};

	    let response = RsqdResponse::ShowJob { job: job };



	    println!("returning job details: {}", details);
	    HttpResponse::Ok().json(
                json!(&response))
	},
	_ => default_handler().await,
    }
}




/// Validates rsq clients
/// validation string
async fn rsq_validate_client(r: &RsqRequest) -> bool {
    if r.validate =="secret" {
	true
    }
    else {
	false
    }
}

#[post("/rsq/request/command")]
async fn rsq_request(cmd: Option<String>) -> HttpResponse {
    let mut response = default_handler().await;
    
    if let Some(rsq_cmd) = cmd {
	match serde_json::from_str::<RsqRequest>(&rsq_cmd) {
	    Ok(rsq) => {
		if rsq_validate_client(&rsq).await {
		    match rsq.cmd {
			RsqCommand::ShowQueue{..} => response = rsq_queue(&rsq).await,
			RsqCommand::ShowHistory{..} => response = rsq_history(&rsq).await,
			RsqCommand::ShowJob{..} => response = rsq_show_job(&rsq).await,
			_ => println!("{:?}", rsq),
		    }

		}
	    },
	    Err(_) => (),
	}
    }

    response
}



#[post("/get/status")]
async fn get_status(params: Option<web::Form<JobStatus>>, req: HttpRequest) -> HttpResponse {
    match params {
        Some(web::Form(p)) => {
            log::info!("Got an post: key={}, jobid={}", p.key, p.jobid);
            HttpResponse::Ok().json(
                json!({
                    "jobid" : p.jobid,
                    "key": p.key
                })
            )
        },
        None => {
            match req.peer_addr() {
                Some(ip) => {
                    log::warn!("get_status called with wrong parameters: from host: {}", ip);
                }
                None => {
                    log::warn!("get_status called with wrong parameters");
                }
            }
            default_handler().await
        }
    }
}

#[get("/get/status/{jobid}")]
async fn get(jobid: web::Path<u32>) -> HttpResponse {
    log::info!("Got an integer: {}", jobid);
    HttpResponse::Ok().json(
        json!({ "jobid" : format!("{}", jobid) })
    )
}

async fn default_handler() -> HttpResponse {
    HttpResponse::Ok().json(json!({}))
}



/// Is called if wrong arguments
/// are specified.
/// Only single (one) argument
/// is valid.
fn print_wrong_args() {
    println!("Wrong arguments. Try -h or --help for help.");
    std::process::exit(2);
}


/// Prints help text and exit
fn print_help() {
    println!("Usage: rsqd [OPTION] ...");
    println!("");
    println!("Execute the RsQueue daemon. Starts in normal mode without arguments.");
    println!("");
    println!("Options:");
    println!("  --validate                  Executes the validate daemon.");
    println!("                              Instructions will be printed at start up.");
    println!("  --list-validate-hosts       List all validated hosts.");
    println!("  --rm-validate-host NAME/IP  Removes specified validated hosts.");
    println!("  --version, -V               Prints version text and exit");
    println!("  -h, --help                  Prints this help text and exit");
    println!("");
    println!("Documentation available locally via: man rsqd");
    println!("Documentation for the entire system can be found at:");
    println!("<https://notabug.org/marcux/rsqueue/wiki>");
    println!("");
    println!("Report bugs to: <https://notabug.org/marcux/rsqueue/issues>");
}



/// Returns the validation string
/// that is required to communicate.
/// The validation string for the RsQueue daemon.
fn get_validate_string() -> String {
    // TODO
    // get string from database
    // if there are no string in database
    // generate a new string and save to database

    let pg = PasswordGenerator {
        length: 50,
        numbers:true,
        lowercase_letters: true,
        uppercase_letters: true,
        symbols: false,
        spaces: false,
        exclude_similar_characters: false,
        strict: true,
    };

    match pg.generate_one() {
        Ok(p) => {
            // save new password to database
            p
        },
        Err(e) => {
            log::error!("Failed to generate new validate string: {}", e);
            String:: from("")
        }
    }
}



/// Called when hosts
/// tries to validate,
/// using the --validate flag.
#[post("/validate")]
async fn validate(req: HttpRequest, password: web::Data<String>, validate: Option<String>) -> HttpResponse {
    let mut response = default_handler().await;
    
    if let Some(host_valid) = validate {
	match serde_json::from_str::<RsqValidateRequest>(&host_valid) {
	    Ok(v) => {
		if v.password == password.to_string() {
                    let connect_info = req.connection_info();
                    
                    if let Some(host_ip) = connect_info.peer_addr() {
                        if let Some(valid_string) = v.validate {
                            if let Ok(ip) = host_ip.parse::< std::net::IpAddr>() {
                                if let Ok(hostname) = lookup_addr(&ip) {
                                    // TODO
                                    // save validate string to db
                                    // and host_ip to db
                                    // and hostname to db
                                }
                                else {
                                    let resp = RsqValidateResponse {
                                        validate: Err(format!("Failed to resolve IP address: {} to remote host", host_ip)),
                                    };
                        
                                    response = HttpResponse::Ok().json(json!(&resp));
                                }
                            }
                            else {
                                let resp = RsqValidateResponse {
                                    validate: Err("Failed to parse IP address of remote host".to_string()),
                                };
                        
                                response = HttpResponse::Ok().json(json!(&resp));
                            }
                        }

                        let resp = RsqValidateResponse {
                            validate: Ok(get_validate_string()),
                        };
                        
                        response = HttpResponse::Ok().json(json!(&resp))
                    }
                    else {
                        let resp = RsqValidateResponse {
                            validate: Err("Failed to get IP address of remote host".to_string()),
                        };
                        
                        response = HttpResponse::Ok().json(json!(&resp));
                    }
                }
	    },
	    Err(_) => (),
	}
    }

    response
}



/// Validate daemon
/// Will try to start
/// on the 10 following
/// ports after the main
/// daemon. Exists if fail.
#[actix_web::main]
async fn start_validate_daemon() -> Result<(), ()> {
    let config;

    if users::get_effective_uid() != 0 {
        eprintln!("Permission denied.");
        return Err(());
    }

    
    match get_config() {
	Ok(c) => config = c,
	Err((c, e)) => {
	    config = c;
	    eprintln!("{e}");
	    eprintln!("Starting with default config ...");
	}
    }


    
    db::init_db("rsqd.db".to_string());

    println!("Starting RSQueue validate daemon");
    println!("");
    println!("Specify temporary password that should be used on the host to be verified");
    println!("when promped for.");
    let password = rpassword::prompt_password("Enter temporary password: ").unwrap();

    
    let mut started_daemon = false;
    let port = config.port.unwrap() + 1;
    for p in port..=port+10 {
        let pw = password.clone();
        if let Ok(v_server) = HttpServer::new(move || {
	    App::new()
	        .service(validate)
                .app_data(web::Data::new(pw.clone()))
	        .default_service(web::to(default_handler))
        }).bind(("0.0.0.0", p)) {
            println!("");
            println!("Starting validate server on port: {}", p);
            println!("");
            println!("#### INSTRUCTIONS ####");
            println!("");
            println!("Go to client host and run as root:");
            println!("rsq conf validate --port {}", p);
            println!("or to validate node-rsqd, run as root:");
            println!("node-rsqd --validate");
            println!("follow the instructions and specify port: {}", p);
            println!("Enter the same password at the prompt.");
            println!("");
            println!("When command prints: Validation SUCCESS!!");
            println!("Validation is done and you can enter CTRL+C to stop this validation server.");
            println!("");
            
            match v_server.workers(1).run().await {
                Ok(_) => {
                    started_daemon = true;
                    break;
                },
                Err(_) => (),
            }
        }
    }

    if ! started_daemon {
        eprintln!("Failed to start validate daemon.");
        eprintln!("Tried ports: {}-{}, without success", port, port+10);
        return Err(());
    }

    Ok(())
}



/// Main daemon that is
/// run as normal operation
/// Starts a web server listening
/// for connections from validated
/// clients and node daemons
#[actix_web::main]
async fn start_daemon() -> std::io::Result<()> {
    let config;

    match get_config() {
	Ok(c) => config = c,
	Err((c, e)) => {
	    config = c;
	    eprintln!("{e}");
	    eprintln!("Starting with default config ...");
	}
    }

    // Init log
    let mut log_builder = env_logger::Builder::new();
    log_builder.format(| buf, record | {
	writeln!(
	    buf,
	    "{}  {:5} => {}",
	    chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
	    record.level(),
	    record.args()
	)
    });
    log_builder.filter_level(log::LevelFilter::Info);

    match std::fs::OpenOptions::new()
	.append(true)
	.create(true)
	.open(config.rsqd_log.clone().unwrap()) {
	    Ok(f) => {
		log_builder.target(env_logger::fmt::Target::Pipe(Box::new(f)));
	    },
	    Err(e) => {
		eprintln!("Failed to open log file: {}", config.rsqd_log.clone().unwrap());
		eprintln!("{}", e);
		eprintln!("Printing log to stdout ...");
		log_builder.target(env_logger::fmt::Target::Stdout);
	    }
	}
	
    log_builder.init();
    
    db::init_db("rsqd.db".to_string());

    info!("Starting RSQueue daemon");
    
    HttpServer::new(move || {
	App::new()
            .wrap(middleware::Logger::default())
	    .service(get)
            .service(get_status)
	    .service(rsq_request)
	    .default_service(web::to(default_handler))
    })
    .bind(("0.0.0.0", config.port.unwrap()))?
    .workers(2)
    .run()
    .await
}


fn main() {
    let args: Vec<String> = std::env::args().collect();

    if args.len() == 1 {
        start_daemon();
    }
    else if args.len() == 2 {
        match args[1].as_str() {
            "-h" | "--help" => print_help(),
            "-V" | "--version" => 
                print_version(
                        env!("CARGO_PKG_AUTHORS"),
                        env!("CARGO_PKG_NAME"),
                        env!("CARGO_PKG_VERSION")
                    ),
            "--validate" => {
                match start_validate_daemon() {
                    Ok(_) => (),
                    Err(_) => std::process::exit(3),
                }
            },
            "--list-validate-hosts" => {
                println!("list validate hosts");
            },
            _ => {
                print_wrong_args();
            },
        }
    }
    else if args.len() == 3 {
        if args[1] == "--rm-validate-host" {
            println!("removing valid host: {}", args[2]);
        }
        else {
            print_wrong_args();
        }
    }
    else {
        print_wrong_args();
    }
}
